// Copyright(c) 2015-present, Gabi Melman & spdlog contributors.
// Distributed under the MIT License (http://opensource.org/licenses/MIT)

#ifndef _ZIP_ARCHIVER_H
#define _ZIP_ARCHIVER_H

#include "archiver.h"
#include <type_traits>

namespace spdlog 
{
    namespace archive
    {
        namespace zip
        {
#include "../zlib/minizip/zip.h"
#include "../zlib/minizip/unzip.h"

            enum class ErrorCode : int
            {
                OK = 0,
                OPEN_ZIP_FILE_FAILED = 1,
                ZIP_FILE_NOT_OPENED = 2,
                TARGET_FILE_NOT_FOUND = 3,
                GET_TARGET_FILE_INFO_FAILED = 4,
                OPEN_FILE_IN_ZIP_FAILED = 5,
                OPEN_TARGET_FILE_FAILED = 6,
                WRITE_TARGET_IN_ZIP_FAILED = 7,
                UNZIP_OPEN_FILE_FAILED = 8
            };

            class zip_archiver final : public archiver 
            {
            private:
                filename_t  filename_;
                int level_;
                std::size_t cache_size_;
                archive_err_handler err_handler_;
                ErrorCode error_;
                std::string err_msg_;

                zipFile zip_file_;

                //unzip var
                std::size_t files_;
                std::size_t bytes_;

            public:
                zip_archiver(const filename_t& file_name, const int level, const std::size_t cache_size, const archive_err_handler& err_func);
                virtual ~zip_archiver();
                virtual bool archive_file(const filename_t& target_file) override;
                virtual std::size_t get_archived_files() const override;
                virtual std::size_t get_archived_bytes() const override;
                virtual std::size_t get_file_size() override;

            private:
                bool open();
                std::size_t update_files(unzFile file);//unzip api
                std::size_t update_bytes(unzFile file);//unzip api
            };

            zip_archiver::zip_archiver(const filename_t& file_name, const int level, const std::size_t cache_size, const archive_err_handler& err_func):
                filename_(std::move(file_name)),
                level_(level),
                cache_size_(cache_size),
                err_handler_(err_func),
                error_(ErrorCode::OK),
                zip_file_(nullptr),
                files_(0),
                bytes_(0)
            {
                if(open() == false)
                {
                    if(err_handler_)
                    {
                        error_ = ErrorCode::OPEN_ZIP_FILE_FAILED;
                        err_msg_ = "zip_archiver::open() failed.";
                        err_handler_((std::size_t)error_, err_msg_);
                    }
                }
            }

            zip_archiver::~zip_archiver()
            {
                if(zip_file_ != nullptr)
                {
                    zipClose(zip_file_, NULL);
                    zip_file_ = nullptr;
                }
            }

            bool zip_archiver::open()
            {
                if(spdlog::details::os::path_exists(filename_))
                {
                    //unzip code start
                    auto unz_file_deleter = [](unzFile unz_file) 
                    { 
                        unzClose(unz_file); 
                    };

                    unzFile file = unzOpen(filename_.c_str());
    
                    if(file == nullptr)
                    {
                        if(err_handler_)
                        {
                            error_ = ErrorCode::UNZIP_OPEN_FILE_FAILED;
                            err_msg_ = "zip_archiver::update_files() unzip open file failed.";
                            err_handler_((std::size_t)error_, err_msg_);
                        }
                    }

                    // RAII: unz_file_guard
                    std::unique_ptr<std::remove_pointer<unzFile>::type, decltype(unz_file_deleter)> unz_file_guard(file, unz_file_deleter);

                    files_ = update_files(file);
                    bytes_ = update_bytes(file);
                    std::size_t file_size = get_file_size();
                    //unzip code end

                    zip_file_ = zipOpen(filename_.c_str(), APPEND_STATUS_ADDINZIP);
                }
                else
                {
                    zip_file_ = zipOpen(filename_.c_str(), APPEND_STATUS_CREATE);
                }
                return zip_file_ != nullptr;
            }

            bool zip_archiver::archive_file(const filename_t& target_file) 
            {
                auto file_deleter = [](FILE* file) 
                { 
                    fclose(file); 
                };

                auto open_file_in_zip_deleter = [](zipFile zip_file) 
                { 
                    zipCloseFileInZip(zip_file); 
                };

                if(zip_file_ == nullptr)
                {
                    if(err_handler_)
                    {
                        error_ = ErrorCode::ZIP_FILE_NOT_OPENED;
                        err_msg_ = "zip_archiver::archive_file() zip file is not opened.";
                        err_handler_((std::size_t)error_, err_msg_);
                    }
                    return false;
                }

                if(spdlog::details::os::path_exists(target_file) == false)
                {
                    if(err_handler_)
                    {
                        error_ = ErrorCode::TARGET_FILE_NOT_FOUND;
                        err_msg_ = "zip_archiver::archive_file() target file not found.";
                        err_handler_((std::size_t)error_, err_msg_);
                    }
                    return false;
                }

                struct stat info = {0};
                if(stat(target_file.c_str(), &info) != 0)
                {
                    if(err_handler_)
                    {
                        error_ = ErrorCode::GET_TARGET_FILE_INFO_FAILED;
                        err_msg_ = "zip_archiver::archive_file() get target file info failed.";
                        err_handler_((std::size_t)error_, err_msg_);
                    }
                    return false;
                }

                tm modfied_time = spdlog::details::os::localtime(info.st_mtime);
                /* logs/mylog.3.txt => logs/mylog.zip/19001210_021030.txt => 1900-12-10_02:10:30 */
                filename_t zip_newfile_name = fmt_lib::format(
                SPDLOG_FMT_STRING(SPDLOG_FILENAME_T("{}{:02d}{:02d}_{:02d}{:02d}{:02d}_{}.txt")),
                modfied_time.tm_year + 1900, modfied_time.tm_mon + 1, modfied_time.tm_mday,
                modfied_time.tm_hour, modfied_time.tm_min, modfied_time.tm_sec, files_);

                zip_fileinfo zip_info = {0};
                tm_zip tmz = {0};
                zip_info.tmz_date = tmz;
                zip_info.dosDate = 0;
                zip_info.internal_fa = 0;
                zip_info.external_fa = 0;
                FILE* file = nullptr;
                size_t bytes = 0;
                std::vector<char> buffer(cache_size_);  // RAII: auto delete buffer;

                int err = zipOpenNewFileInZip(zip_file_, zip_newfile_name.c_str(), &zip_info, NULL, 0, NULL, 0, NULL, Z_DEFLATED, level_);
                if(err != ZIP_OK)
                {
                    if(err_handler_)
                    {
                        error_ = ErrorCode::OPEN_FILE_IN_ZIP_FAILED;
                        err_msg_ = "zip_archiver::archive_file() open file in zip failed.";
                        err_handler_((std::size_t)error_, err_msg_);
                    }
                    return false;
                }

                // RAII: open_file_in_zip_guard
                std::unique_ptr<std::remove_pointer<zipFile>::type, decltype(open_file_in_zip_deleter)> open_file_in_zip_guard(zip_file_, open_file_in_zip_deleter);

                file = fopen(target_file.c_str(), "rb");

                if(file == nullptr)
                {
                    if(err_handler_)
                    {
                        error_ = ErrorCode::OPEN_TARGET_FILE_FAILED;
                        err_msg_ = "zip_archiver::archive_file() open target file failed.";
                        err_handler_((std::size_t)error_, err_msg_);
                    }
                    return false;
                }

                std::unique_ptr<FILE, decltype(file_deleter)> file_guard(file, file_deleter);// RAII: file_guard

                while ((bytes = fread(buffer.data(), 1, cache_size_, file)) > 0) 
                {
                    err = zipWriteInFileInZip(zip_file_, buffer.data(), (unsigned int)bytes);
                    if (err != ZIP_OK) 
                    {
                        if(err_handler_)
                        {
                            error_ = ErrorCode::WRITE_TARGET_IN_ZIP_FAILED;
                            err_msg_ = "zip_archiver::archive_file() write target in zip failed.";
                            err_handler_((std::size_t)error_, err_msg_);
                        }
                        return false;
                    }
                    bytes_ += bytes;
                }

                error_ = ErrorCode::OK;
                err_msg_.clear();
                ++files_;
                return true;
            }

            std::size_t zip_archiver::get_archived_files()const
            {
                return files_;
            }

            std::size_t zip_archiver::get_archived_bytes()const
            {
                return bytes_;
            }

            std::size_t zip_archiver::get_file_size()
            {
                struct stat st;
                if (stat(filename_.c_str(), &st) == 0) 
                {
                    return (std::size_t)st.st_size;
                }
                return 0;
            }

            //unzip api
            std::size_t zip_archiver::update_files(unzFile file)
            {
                unz_global_info global_info = {0};
                if(unzGetGlobalInfo(file, &global_info) != UNZ_OK)
                {
                    return 0;
                }

                return static_cast<std::size_t>(global_info.number_entry);
            }

            //unzip api
            std::size_t zip_archiver::update_bytes(unzFile file)
            {
                std::size_t bytes = 0;
                unz_file_info file_info = {0};
                char file_name[MAX_PATH] = {0};

                while(unzGetCurrentFileInfo(file, &file_info, file_name, MAX_PATH, NULL, 0, NULL, 0) == UNZ_OK)
                {
                    bytes += file_info.uncompressed_size;
                    if(unzGoToNextFile(file) != UNZ_OK)
                    {
                        break;
                    }
                }
                return bytes;
            }


            inline archiver* create_zip_archiver(const filename_t& file_name, const int level, const std::size_t cache_size, const archive_err_handler& err_func) 
            {
                return new zip::zip_archiver(file_name, level, cache_size, err_func);
            }

            inline void destroy_zip_archiver(archiver* archiver_ptr)
            {
                if(archiver_ptr)
                {
                    zip::zip_archiver* zip_archiver_ptr = dynamic_cast<zip::zip_archiver*>(archiver_ptr);
                    delete zip_archiver_ptr;
                }
            }
        }  // namespace zip
    }   // namespace archiver
}   // namespace spdlog



#endif  // _ZIP_ARCHIVER_H

